<?php
// {{{ Header
/*
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
 | SIERRA : PHP Application Framework  http://code.google.com/p/sierra-php |
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
 | Copyright 2005 Jason Read                                               |
 |                                                                         |
 | Licensed under the Apache License, Version 2.0 (the "License");         |
 | you may not use this file except in compliance with the License.        |
 | You may obtain a copy of the License at                                 |
 |                                                                         |
 |     http://www.apache.org/licenses/LICENSE-2.0                          |
 |                                                                         |
 | Unless required by applicable law or agreed to in writing, software     |
 | distributed under the License is distributed on an "AS IS" BASIS,       |
 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.|
 | See the License for the specific language governing permissions and     |
 | limitations under the License.                                          |
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
 */
// }}}

// {{{ Constants

/**
 * SRA_XmlParser class debug flag
 * 
 * @type   String
 * @access public
 */
define('SRA_XMLPARSER_DEBUG', FALSE);
// }}}

// {{{ Includes
// }}}

// {{{ SRA_XmlParser
/**
 * This class is used to parse xml files, maintain xml data structures,
 * cache xml data structures, and write data to xml files. It also
 * performs some xml file validation including checking for existence of
 * the required header. All xml tags are case insensitive. This parser
 * uses only lower-case xml tags.
 *
 * @author    Jason Read <jason@idir.org>
 * @package sierra.util
 */
class SRA_XmlParser {
    // {{{ Properties
    
    /**
     * used to store any errors generated by this class constructor
     * @type   Object
     * @access public
     */
    var $err = NULL;
    
    /**
     * Static array of SRA_XmlParser objects used as cache by the getXmlParser
     * static method.
	   * 
	   * STATIC: Maintained in getXmlParser method
	   * 
     * @type   SRA_XmlParser[]
     * @access private
     var $_cachedXmlParser;
	   */
	 
    /**
     * The name of the cached xml array file.
     * @type   String
     * @access private
     */
    var $_cacheFileName;
	 
    /**
     * Attribute used to store the data for the xml file. This will be an
     * associative data structure (key=element).
     * @type   Object
     * @access private
     */
    var $_data;
    
    /**
     * Whether or not the root tag was included in the associative array
     * data structure when the file was parsed.
     * @type   boolean
     * @access private
     */
    var $_includeRootTag;
    /**
     * The value of the root tag.
     * @type   String
     * @access private
     */
    var $_rootTag;
    /**
     * Used by the _process* methods to maintain the relevant parse related
     * data. This attribute will be an associative array with the
     * following sub-elements:
     * 
     * val
     * currTag
     * levels
     * prevTag
     * multipleData
     * xml
     * 
     * @type   Object
     * @access private
     */
    var $_xmlParseData = array();
    /**
     * The name of the file from which the xml data was extracted.
     * @type   String
     * @access private
     */
    var $_xmlParserFile;
    // }}} 

    // {{{ SRA_XmlParser()
    /**
     * Class constructor. This method loads the data from the xml file
     * specified into the _data attribute as an associative multi-level
     * array. This parser uses all lower-case xml tags even if the tags
     * are otherwise written in the source xml file. This method is
     * private and should not be directly accessed. Instead, new SRA_XmlParser
     * objects should be retrieved through the SRA_XmlParser::getXmlParser
     * static method.
     *
     * @param   xmlParserFile : String - The name of the file to parse.
     * This name must include the .xml file extension. alternatively, this can 
     * be an xml string
     * @param   includeRootTag : boolean - Whether or not the root tag
     * should be included in the associative array this method generates
     * (all data will then be accessible through
     * $objectReference["root_tag"]["..."]).
     * @param   createFile : boolean - Whether or not the file should be
     * created if it does not exist. By default this parameter is false.
     * @param   rootTag : String - The root tag to use if the createFile
     * parameter is true.
     * @access  private
     * @return  
     * @author  Jason Read <jason@idir.org>
     */
    function SRA_XmlParser($xmlParserFile, $includeRootTag=FALSE, $createFile=FALSE, $rootTag=FALSE)
    {
			// Check if file exists
			if (!file_exists($xmlParserFile) && !strstr($xmlParserFile, 'http') && !strstr($xmlParserFile, 'ftp') && !strpos($xmlParserFile, '>'))
			{
				// SRA_File does not exist and should not be created
				if (!$createFile)
				{
					$msg = "SRA_XmlParser::SRA_XmlParser: Failed - SRA_File '${xmlParserFile}' does not exist and will not be created.";
					$this->err = SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
					return;
				}
				// Root tag was not specified, file cannot be created
				else if (!$rootTag)
				{
					$msg = "SRA_XmlParser::SRA_XmlParser: Failed - SRA_File '${xmlParserFile}' cannot be created, rootTag parameter was not specified.";
					$this->err = SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
					return;
				}
				
				// SRA_File cannot be written
				if (SRA_Error::isError(SRA_File::write($xmlParserFile, $rbuffer)))
				{
					$msg = "SRA_XmlParser::SRA_XmlParser: Failed - SRA_File '${xmlParserFile}' could not be written to.";
					$this->err = SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
					return;
				}
				
				$msg = "SRA_XmlParser::SRA_XmlParser: SRA_File '${xmlParserFile}' does not exist and was created.";
				SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
			}
			
			$this->_xmlParserFile = $xmlParserFile;
			$this->_includeRootTag = $includeRootTag;
			$this->_rootTag = $rootTag;
			
			// Check cache for array
      if (is_file($this->_xmlParserFile)) {
        $this->_cacheFileName = SRA_TMP_DIR . '/' . str_replace('/', '.', $xmlParserFile) . $includeRootTag . '.php';
        if (file_exists($this->_cacheFileName))
        {
          // Verify that cache file is up to date
          if (SRA_File::compareMTimes($this->_cacheFileName, $xmlParserFile) != -1)
          {
            include($this->_cacheFileName);
            if (!isset($xml) || !is_array($xml))
            {
              unlink($this->_cacheFileName);
              $msg = 'SRA_XmlParser::SRA_XmlParser: Failed - Cache file "' . $this->_cacheFileName . 
                   '" does not contain valid data. Deleting, and continuing.';
              SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
            }
            else
            {
              $xmlKeys = array_keys($xml);
              $this->_rootTag = $xmlKeys[0];
              if ($this->_includeRootTag)
              {
                $this->_data =& $xml;
              }
              else
              {
                $this->_data =& $xml[$this->_rootTag];
              }
              $msg = "SRA_XmlParser::SRA_XmlParser: Accessing xml array from cache ($this->_cacheFileName) for file: ${xmlParserFile}";
              SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
              return;
            }
          }
          else
          {
            unlink($this->_cacheFileName);
            $msg = 'SRA_XmlParser::SRA_XmlParser: Cache file "' . $this->_cacheFileName . '" has expired. Deleting and continuing.';
            SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
          }
        }
      }
			
			// Parse file
			if (SRA_Error::isError($this->_process()))
			{
				$msg = "SRA_XmlParser::SRA_XmlParser: Failed - SRA_File '${xmlParserFile}' could not be parsed. _process method returned an error";
				$this->err = SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
				return;
			}
			else
			{
				unset($this->_xmlParseData);
			}
    }
    // }}}
	
    // {{{ arrayToXML()
    /**
     * Static method used to convert an associative array back to xml format. 
     *
	 * @param	array. Object. The associative array to convert. values in this array 
   * should be properly encoded already
	 * @param	margin. int. Used to inset each level of tags.
	 * @param	parentKey. String. The key used by the parent of the array.
     * @access  public
     * @return  String
     * @author  Jason Read <jason@idir.org>
     */
    function & arrayToXML($array, $margin = 0, $parentKey = false)
    {
        $keys = array_keys($array);
		$xmlBuffer = '';
		$spaceBuffer = '';
		for ($i=0; $i<$margin; $i++)
		{
			$spaceBuffer .= '  ';
		}
		
		foreach ($keys as $key)
		{
			$attrKey = $key;
			$data = $array[$key];
			
			// 2..* values
			if (is_array($array[$key]))
			{
				$skeys = array_keys($array[$key]);
				if ($skeys[0] == '0' && !isset($array[$key][$skeys[0]]['attributes']))
				{
					foreach ($skeys as $skey)
					{
            // render open
            if ($skey == '0' && is_array($array[$key][$skey])) {
              $xmlBuffer .= "${spaceBuffer}<${key}>\n";
            }
						$msg = "SRA_XmlParser::arrayToXML: Key '${key}' contains 2..* sub elements. Adding '${skey}'";
						SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
						$temp = !is_array($array[$key][$skey]) ? array($key => $array[$key][$skey]) : $array[$key][$skey];
						$xmlBuffer .= SRA_XmlParser::arrayToXML($temp, !is_array($array[$key][$skey]) ? $margin : $margin + 1, $key);
            // render close
            if ($skey == $skeys[count($skeys) - 1] && is_array($array[$key][$skey])) {
              $xmlBuffer .= "${spaceBuffer}</${key}>\n";
            }
					}
					continue;
				}
				else if (is_array($array[$key][$skeys[0]]) && array_key_exists('attributes', $array[$key][$skeys[0]]) && 
						 is_array($array[$key][$skeys[0]]['attributes']) && array_key_exists('key', $array[$key][$skeys[0]]['attributes']))
				{
					foreach ($skeys as $skey)
					{
						$msg = "SRA_XmlParser::arrayToXML: Key '${key}' contains 1..* sub elements with keys. Adding '${skey}'";
						SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
						$temp = array($key => $array[$key][$skey]);
						$xmlBuffer .= SRA_XmlParser::arrayToXML($temp, $margin, $key);
					}
					continue;
				}
			}
			
			// Add attributes
			if (is_array($array[$key]) && array_key_exists('attributes', $array[$key]))
			{
        $data = isset($array[$key]['xml_value']) ? $array[$key]['xml_value'] : $data;
        
				$msg = "SRA_XmlParser::arrayToXML: Key '${key}' contains attributes.";
				SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
				$akeys = array_keys($array[$key]['attributes']);
				$ukey = false;
				foreach ($akeys as $akey)
				{
          if ($akey == '_akey' || ($akey == 'key' && in_array('_akey', $akeys))) { continue; }
					$xmlBuffer .= ' ' . $akey . '="' . htmlspecialchars($array[$key]['attributes'][$akey], ENT_COMPAT) . '"';
				}
				if (count($array[$key]) == 1)
				{
					$msg = "SRA_XmlParser::arrayToXML: Key '${key}' is an empty tag.";
					SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
					$xmlBuffer = $spaceBuffer . '<' . $attrKey . $xmlBuffer . "/>\n";
					continue;
				}
			}
			
			$xmlBuffer = $spaceBuffer . '<' . $attrKey . $xmlBuffer . '>';
			
			// Add data
			if (!is_array($data))
			{
				$msg = "SRA_XmlParser::arrayToXML: Key '${key}' is a data tag.";
				SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
				$xmlBuffer .= $data . '</' . $attrKey . ">\n";
			}
			// Add sub-elements
			else
			{
				$ukeys = array_keys($data);
				$xmlBuffer .= "\n";
				foreach ($ukeys as $ukey)
				{
					if ($ukey != 'attributes')
					{
						$msg = "SRA_XmlParser::arrayToXML: Key '${key}' contains distinct sub elements. Adding '${ukey}'";
						SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
						$temp = array($ukey => $data[$ukey]);
						$xmlBuffer .= SRA_XmlParser::arrayToXML($temp, $margin + 1, $attrKey);
					}
				}
				$xmlBuffer .= $spaceBuffer . '</' . $attrKey . ">\n";
			}
		}
		return $xmlBuffer;
    }
    // }}}
		
    // {{{ deleteCache()
    /**
     * Used to delete a cached xml file
     *
     * @param   xmlParserFile : String - path to the xml file
     * @param   includeRootTag : boolean - Whether or not the root tag
     * should be included
     * @access  public
     * @return  boolean
     * @author  Jason Read <jason@idir.org>
     */
    function deleteCache($xmlParserFile, $includeRootTag=FALSE)
    {
			// Check cache for array
			$cacheFileName = SRA_Controller::getSysTmpDir() . '/' . str_replace('/', '.', $xmlParserFile) . $includeRootTag . '.php';
			if (file_exists($cacheFileName)) {
				unlink($cacheFileName);
        return TRUE;
			}
      return FALSE;
    }
    // }}}
	
    // {{{ getCacheFileName()
    /**
     * Returns the value of the _cacheFileName attribute.
     *
     * @access  public
     * @return  String
     * @author  Jason Read <jason@idir.org>
     */
    function getCacheFileName()
    {
        if (isset($this->_cacheFileName))
        {
            return($this->_cacheFileName);
        }
    }
    // }}}

    // {{{ getData()
    /**
     * Returns a reference to the _data attribute or to the keys data
     * specified by the sub parameter. If the requested element is a data 
	 * element (contains no sub-elements) with attributes, then only the 
	 * data will be returned.
     *
     * @param   keys : String[] - An array of strings representing a sub
     * element that should be returned from the _data attribute. If the
     * sub-element does not exist, this method will return an OPERATIONAL
     * level error. For example, if the _data structure was of the format:
     * 
     * array( "root" => array("level1" => array("level2" => "text")))
     * 
     * and the level2 element was needed, this parameter would be of the
     * following format:
     * 
     * keys = array("root", "level1", "level2")
     * 
     * The order of this parameter is important. Using the example above,
     * the following parameter would generate an SRA_Error:
     * 
     * keys = array("root", "level2", "level1")
     * 
     * because there is not "level2" node within the "root" node.
	 * 
	 * @param	importFile : boolean - Whether or not the return value 
	 * 			should be the contents of a file if the data references 
	 * 			a file. SRA_File data elements are designated by setting the 
	 * 			'is_file' attribute to 1. The default value for this 
	 * 			parameter is true.
     * @access  public
     * @return  Object
     * @author  Jason Read <jason@idir.org>
     */
    function & getData($keys=false, $importFile=true)
    {
        if (isset($this->_data))
        {
			// Convert single elements to an array
			if ($keys && !is_array($keys))
			{
				$keys = array($keys);
			}
			
			// Return all data
			if (!$keys)
			{
				return $this->_data;
			}
			// Return sub-data
			else
			{
				$xml =& $this->_data;
				foreach ($keys as $key)
				{
					// SRA_Error
					if (!is_scalar($key) || !is_array($xml))
					{
						$msg = 'SRA_XmlParser::getData: Failed - Invalid data types for xml file: "' . 
								$this->_xmlParserFile . '" - key: ' . gettype($key) . ' xml: ' . gettype($xml);
						return SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
					}
					// Sub data does not exist
					if (!array_key_exists($key, $xml))
					{
						$msg = "SRA_XmlParser::getData: Failed - Sub-key '${key}' does not exist (Sub-keys - " . implode('::', $keys) . ') in xml file: "' . 
								$this->_xmlParserFile . '"';
						return SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_OPERATIONAL, SRA_XMLPARSER_DEBUG);
					}
					else
					{
						$temp =& $xml;
						$xml =& $temp[$key];
					}
				}
				$msg = 'SRA_XmlParser::getData: Sub-keys - "' . implode('::', $keys) . '" found in xml file: "' . $this->_xmlParserFile . '"';
				SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
				
				// Import file if specified
				if ($importFile && array_key_exists('attributes', $xml) && array_key_exists('is_file', $xml['attributes']) && 
					$xml['attributes']['is_file'] == 1)
				{
					if (file_exists($xml['xml_value']))
					{
						$msg = 'SRA_XmlParser::getData: Returning contents of file: "' . $xml['xml_value'] . '"';
						SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
						return SRA_File::toString($xml['xml_value']);
					}
					else
					{
						$msg = 'SRA_XmlParser::getData: SRA_Error - Sub-keys - ' . implode('::', $keys) . ') in xml file: "' . 
								$this->_xmlParserFile . '" specify is_file=1 for file that does not exist. SRA_File: "' . 
								$xml['xml_value'] . '", returning element data instead';
						SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
					}
				}
				
				// Only return data if data element with attributes
				if (array_key_exists('xml_value', $xml) && (!isset($xml['attributes']) || !count($xml['attributes'])))
				{
					$msg = 'SRA_XmlParser::getData: Data element found with attributes. Returning data only.';
					SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
					return $xml['xml_value'];
				}
				else
				{
					return $xml;
				}
			}
        }
    }
    // }}}
    
  // {{{ setData
  /**
   * sets the xml data
   * @param mixed $data the data to set
   * @access public
   * @return void
   */
  function setData(& $data) {
    $this->_data =& $data;
  }
  // }}}
	
    // {{{ getDataAttributes()
    /**
     * Similiar to the getData method except that this method returns the 
	 * attributes associated with the data. It returns an empty array if 
	 * none exist.
     *
     * @param   keys : String[] - An array of strings representing a sub 
	 * 			element that should be returned from the _data attribute. 
	 * 			See SRA_XmlParser::getData api for more information.
     * @access  public
     * @return  Object
     * @author  Jason Read <jason@idir.org>
     */
    function & getDataAttributes($keys=false)
    {
        if (isset($this->_data))
        {
			// Convert single elements to an array
			if ($keys && !is_array($keys))
			{
				$keys = array($keys);
			}
			
			// Return all data
			if (!$keys)
			{
				return $this->_data;
			}
			// Return sub-data
			else
			{
				$xml =& $this->_data;
				foreach ($keys as $key)
				{
					// Sub data does not exist
					if (!array_key_exists($key, $xml))
					{
						$msg = "SRA_XmlParser::getDataAttributes: Failed - Sub-key '${key}' does not exist (Sub-keys - " . implode('::', $keys) . ") in xml file: '" . 
								$this->_xmlParserFile . "'";
						return SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_OPERATIONAL, SRA_XMLPARSER_DEBUG);
					}
					else
					{
						$temp =& $xml;
						$xml =& $temp[$key];
					}
				}
				$msg = 'SRA_XmlParser::getDataAttributes: Sub-keys - "' . implode('::', $keys) . '" found in xml file: "' . $this->_xmlParserFile . '"';
				SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
				
				// Only return data if data element with attributes
				if (array_key_exists('attributes', $xml))
				{
					$msg = 'SRA_XmlParser::getDataAttributes: Attributes found. Returning';
					SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
					return $xml['attributes'];
				}
				else
				{
					$msg = 'SRA_XmlParser::getDataAttributes: Attributes NOT found. Returning empty array.';
					SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
					return array();
				}
			}
        }
    }
    // }}}

    // {{{ getRootTag()
    /**
     * Returns the value of the _rootTag attribute.
     *
     * @access  public
     * @return  String
     * @author  Jason Read <jason@idir.org>
     */
    function getRootTag()
    {
        if (isset($this->_rootTag))
        {
            return($this->_rootTag);
        }
    }
    // }}}

    // {{{ getXmlParser()
    /**
     * Static method used to instantiate a new SRA_XmlParser object, or return
     * an existing one. This method maintains a cached array of SRA_XmlParser
     * objects. If one is requested that has already been instantiated, a
     * reference to the existing object is returned. Otherwise, an new
     * object is instantiated, added to the cache, and returned as a
     * reference.
     *
     * @param   xmlParserFile : String - The name of the file to parse. See
     * constructor api for more info. alternative, this can be an xml string
     * @param   includeRootTag : boolean - Whether or not to include the
     * root tag in the data structure. See constructor api for more info.
     * @param   createFile : boolean - Whether or not the file should be
     * created if it does not exist. See constructor api for more info.
     * @param   rootTag : String - The value of the root tag (only applies
     * if creating a new xml file). See constructor api for more info.
     * @param boolean $noCache whether or not to return a cached parser if 
     * available
     * @access  public
     * @return  SRA_XmlParser
     * @author  Jason Read <jason@idir.org>
     */
    function & getXmlParser($xmlParserFile, $includeRootTag=FALSE, $createFile=FALSE, $rootTag=NULL, $noCache=FALSE)
    {
			
			static $_cachedXmlParser = array();
			
			if (!is_file($xmlParserFile) || !array_key_exists($xmlParserFile . $includeRootTag, $_cachedXmlParser) || $noCache) {
        if (SRA_XMLPARSER_DEBUG) {
          $msg = 'SRA_XmlParser::getXmlParser: Instantiating new SRA_XmlParser object for file: "' . $xmlParserFile . '"';
          SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
        }
        $parser = new SRA_XmlParser($xmlParserFile, $includeRootTag, $createFile, $rootTag);
        
        if (is_file($xmlParserFile)) {
          $_cachedXmlParser[$xmlParserFile . $includeRootTag] =& $parser;
        }
				
        if (!SRA_XmlParser::isValid($parser)) {
					$msg = 'SRA_XmlParser::getXmlParser: Failed - Could not instantiate new SRA_XmlParser object for file: "' . $xmlParserFile . '"';
					return SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
				}
				else if (SRA_XMLPARSER_DEBUG) {
					$msg = 'SRA_XmlParser::getXmlParser: SRA_XmlParser object instantiated successfully for file: "' . $xmlParserFile . '"';
					SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
				}
        return $parser;
			}
			else {
				$msg = 'SRA_XmlParser::getXmlParser: Returning cached SRA_XmlParser object for file: "' . $xmlParserFile . '"';
				SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
			}
      
			return $_cachedXmlParser[$xmlParserFile . $includeRootTag];
    }
    // }}}

    // {{{ getXmlParserFile()
    /**
     * Returns the value of the _xmlParserFile attribute.
     *
     * @access  public
     * @return  String
     * @author  Jason Read <jason@idir.org>
     */
    function getXmlParserFile()
    {
        if (isset($this->_xmlParserFile))
        {
            return($this->_xmlParserFile);
        }
    }
    // }}}
		
  // {{{ isCached
  /**
   * used to check if an xml file is cached
   * @param string $xmlParserFile path to the xml file
   * @param boolean $includeRootTag whether or not the root tag should be 
   * included
   * @access public
   * @return boolean
   */
  function isCached($xmlParserFile, $includeRootTag=FALSE) {
    // Check cache for array
    $cacheFileName = SRA_Controller::getSysTmpDir() . '/' . str_replace('/', '.', $xmlParserFile) . $includeRootTag . '.php';
    if (file_exists($cacheFileName)) {
      return SRA_File::compareMTimes($cacheFileName, $xmlParserFile) != -1 ? TRUE : FALSE;
    }
    return FALSE;
  }
  // }}}
	
    // {{{ isDirty()
    /**
     * Returns true if the data associated with this SRA_XmlParser is dirty, 
	 * false otherwise. 
     *
     * @access  public
     * @return  boolean
     * @author  Jason Read <jason@idir.org>
     */
    function isDirty()
    {
        if (SRA_XmlParser::isValid($xmlParser = new SRA_XmlParser($this->_xmlParserFile, $this->_includeRootTag, false, false)))
		{
			return ($this->_data != $xmlParser->getData());
		}
		// SRA_XmlParser object copy could not be instantiated, generate SRA_Error and return false
		else
		{
			$msg = 'SRA_XmlParser::isDirty: Failed - Could not instantiate SRA_XmlParser object copy';
			SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
		}
		return false;
    }
    // }}}

    // {{{ isIncludeRootTag()
    /**
     * Returns the value of the _includeRootTag attribute.
     *
     * @access  public
     * @return  boolean
     * @author  Jason Read <jason@idir.org>
     */
    function isIncludeRootTag()
    {
        if (isset($this->_includeRootTag))
        {
            return($this->_includeRootTag);
        }
    }
    // }}}
	
    // {{{ isValid()
    /**
     * Static method used to validate a SRA_XmlParser object. 
     *
     * @param   object. Object. The object to validate.
     * @access  public
     * @return  boolean
     * @author  Jason Read <jason@idir.org>
     */
    function isValid(& $object)
    {
        return (is_object($object) && (!isset($object->err) || !SRA_Error::isError($object->err)) && strtolower(get_class($object)) == 'sra_xmlparser');
    }
    // }}}

  // {{{ write
  /**
   * This method writes the current SRA_XmlParser data to the associated xml and 
   * cache files overwriting any existing data in those file.
   * @param string $xmlFile an optional parameter specifying another xml file to
   * write to. If this parameter is not specified, the $xmlFile specified in the 
   * constructor will be written to
   * @param string $header an optional header to include when writing to the 
   * xml file
   * @access public
   * @return void
   */
  function write($xmlFile=NULL, $header=NULL) {
    $xmlFile  = $xmlFile ? $xmlFile : $this->_xmlParserFile;
    
    $xml = !$this->_includeRootTag ? array($this->_rootTag => $this->_data) : $this->_data;
    SRA_Util::printDebug('SRA_XmlParser::write: Writing XML structure - ', SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
    SRA_Util::printDebug($xml, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
    if (SRA_Error::isError($rbuffer =& SRA_XmlParser::arrayToXML($xml))) {
      $msg = 'SRA_XmlParser::write: Failed - Could not get xml buffer (SRA_XmlParser::arrayToXML failed) for file: "' . $xmlParserFile . '"';
      return SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
    }
    else {
      $msg ='SRA_XmlParser::write: xml buffer created, writing xml back to file: ' . $this->_xmlParserFile;
      SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
      if ($header) $rbuffer = $header . $rbuffer;
      if (SRA_Error::isError(SRA_File::write($xmlFile, $rbuffer))) {
        $msg = 'SRA_XmlParser::write: Failed - Could not write xml buffer to file: "' . $xmlParserFile . '"';
        return SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
      }
      else {
        $this->_cache();
      }
    }
  }
  // }}}
	
    // {{{ _cache()
    /**
     * Writes the existing SRA_XmlParser data to a cache file.  
     *
     * @access  public
     * @return  void
     * @author  Jason Read <jason@idir.org>
     */
    function _cache() {
      if ($this->_cacheFileName) {
        // Cache array to file for future reference
        if (SRA_XMLPARSER_DEBUG) {
          $msg ='SRA_XmlParser::_cache: Writing xml array to cache for file: ' . $this->_xmlParserFile;
          SRA_Util::printDebug($msg, SRA_XMLPARSER_DEBUG, __FILE__, __LINE__);
        }
        $xml = !$this->_includeRootTag ? array($this->_rootTag => & $this->_data) : $xml = & $this->_data;
        if (is_array($xml)) {
          SRA_File::arrayToFile($this->_cacheFileName, 'xml', $xml);
          exec('chmod 666 ' . $this->_cacheFileName);
        }
        else {
          $msg = 'SRA_XmlParser::_cache: Failed - _data attribute is not an array: "' . gettype($this->_data) . '"';
          return SRA_Error::logError($msg, __FILE__, __LINE__, SRA_ERROR_PROBLEM, SRA_XMLPARSER_DEBUG);
        }
      }
    }
    // }}}
    
    // {{{ _checkForSingleLevel()
    /**
     * checks for and promotes single level elements in $data
     *
     * @access  public
     * @return  void
     */
    function _checkForSingleLevel(& $data) {
      if (is_array($data) && count($data) == 1 && ($keys = array_keys($data)) && isset($data[$keys[0]]) && isset($data[$keys[0]]['attributes']) && isset($data[$keys[0]]['attributes']['_akey'])) {
        $ukey = $keys[0];
        $keys = array_keys($data[$ukey]);
        foreach($keys as $key) {
          $data[$key] = $data[$ukey][$key];
        }
        unset($data[$ukey]);
      }
      if (is_array($data)) {
        $keys = array_keys($data);
        foreach($keys as $key) {
          $this->_checkForSingleLevel($data[$key]);
        }
      }
    }
    // }}}
	
    // {{{ _getKey()
    /**
     * This method is used to provide the functionality for retrieving an 
	 * xml special attribute 'key' which defines the index key to use in 
	 * the resultant associative array. 
     *
     * @param   tag : String - The tag to get the data for. If data does 
	 * not exist for this tag, the value of this tag will be returned. 
     * @access  public
     * @return  void
     * @author  Jason Read <jason@idir.org>
     */
    function _getKey($tag)
    {
		$key = '';
		$limit = count($this->_xmlParseData['keys']);
		for ($i=$this->_xmlParseData['key']; $i<$limit; $i++)
		{
			
			// Data found
			if ($this->_xmlParseData['vals'][$i]['type'] == 'complete' && 
				strtolower($this->_xmlParseData['vals'][$i]['tag']) == strtolower($tag) && 
				isset($this->_xmlParseData['vals'][$i]['value']))
			{
				$key = $this->_xmlParseData['vals'][$i]['value'];
				break;
			}
			
			// End found
			if ($this->_xmlParseData['vals'][$i]['type'] == 'close' && 
				$this->_xmlParseData['vals'][$i]['tag'] == $this->_xmlParseData['val']['tag'])
			{
				break;
			}
		}
		// If data not found, return value of tag
		if ($key == '')
		{
			$key = $tag;
		}
		$key = preg_replace_callback("'php::(.*?)::php'si", 'codeToString', $key);
		return $key;
    }
    // }}}

    // {{{ _process()
    /**
     * Method used to begin parsing of an xml file. Also manages xml file
     * cache.
     *
     * @access  private
     * @return  void
     * @author  Jason Read <jason@idir.org>
     */
    function _process()
    {
		
		$p = xml_parser_create();
		xml_parse_into_struct($p, (is_file($this->_xmlParserFile) ? SRA_File::toString($this->_xmlParserFile) : $this->_xmlParserFile), $this->_xmlParseData['vals'], $index);
		xml_parser_free($p);
    
		// add dynamic keys
		$keyptr = array(0);
		$vkeys = array_keys($this->_xmlParseData['vals']);
		foreach ($vkeys as $vkey) {
			if ($this->_xmlParseData['vals'][$vkey]['type'] == 'open') {
				array_push($keyptr, 0);
			}
			if (!isset($this->_xmlParseData['vals'][$vkey]['attributes']['KEY']) && ($this->_xmlParseData['vals'][$vkey]['type'] == 'complete' || $this->_xmlParseData['vals'][$vkey]['type'] == 'open')) {
        if (!isset($this->_xmlParseData['vals'][$vkey]['attributes'])) { $this->_xmlParseData['vals'][$vkey]['attributes'] = array(); }
				$this->_xmlParseData['vals'][$vkey]['attributes']['KEY'] = $this->_xmlParseData['vals'][$vkey]['type'] == 'complete' ? $keyptr[count($keyptr) - 1]++ : $keyptr[count($keyptr) - 2]++;
        $this->_xmlParseData['vals'][$vkey]['attributes']['_AKEY'] = '1';
			}
      if ($this->_xmlParseData['vals'][$vkey]['type'] == 'close') {
				array_pop($keyptr);
			}
		}
		
		$this->_data = array();
		$this->_xmlParseData['levels'] = array();
		$this->_xmlParseData['multipleData'] = array();
		$this->_xmlParseData['prevTag'] = '';
		$this->_xmlParseData['currTag'] = '';
		$this->_xmlParseData['topTag'] = false;
		$this->_xmlParseData['keys'] = array_keys($this->_xmlParseData['vals']);
    
		foreach ($this->_xmlParseData['keys'] as $this->_xmlParseData['key'])
		{
			// Set data
			$this->_xmlParseData['val'] =& $this->_xmlParseData['vals'][$this->_xmlParseData['key']];
			
			// Open tag
			if ($this->_xmlParseData['val']['type'] == 'open')
			{
				if (!$this->_processOpen())
				{
					continue;
				}
			}
			// Close tag
			else if ($this->_xmlParseData['val']['type'] == 'close')
			{
				if (!$this->_processClose())
				{
					continue;
				}
			}
			// Data tag
			else if ($this->_xmlParseData['val']['type'] == 'complete' && isset($this->_xmlParseData['val']['value']))
			{
				$loc =& $this->_data;
				foreach ($this->_xmlParseData['levels'] as $level)
				{
					$temp =& $loc[str_replace(':arr#', '', $level)];
					$loc =& $temp;
				}
				$tag = strtolower($this->_xmlParseData['val']['tag']);
				
				// Check for embedded php code
				if (strstr($this->_xmlParseData['val']['value'], 'php::'))
				{
					$this->_xmlParseData['val']['value'] = preg_replace_callback("'php::(.*?)::php'si", 'codeToString', 
																				 $this->_xmlParseData['val']['value']);
				}
				
				// Check for attributes or attribute key
				$addAttributes = false;
				unset($dataKey);
				if (array_key_exists('attributes', $this->_xmlParseData['val']))
				{
					if (array_key_exists('KEY', $this->_xmlParseData['val']['attributes']))
					{
						$dataKey = $this->_getKey($this->_xmlParseData['val']['attributes']['KEY']);
					}
					if (count($this->_xmlParseData['val']['attributes']) >= 1)
					{
						$keys = array_keys($this->_xmlParseData['val']['attributes']);
						foreach ($keys as $key)
						{
							// Check for embedded php code
							if (strstr($this->_xmlParseData['val']['attributes'][$key], 'php::'))
							{
								$this->_xmlParseData['val']['attributes'][$key] = preg_replace_callback("'php::(.*?)::php'si", 'codeToString', 
																							 $this->_xmlParseData['val']['attributes'][$key]);
							}
							$this->_xmlParseData['val']['attributes'][strtolower($key)] = $this->_xmlParseData['val']['attributes'][$key];
							unset($this->_xmlParseData['val']['attributes'][$key]);
						}
						$addAttributes = true;
					}
				}
				
				// Add to array
				if ((is_array($loc) && array_key_exists($tag, $loc)) || isset($dataKey))
				{
					if (isset($loc[$tag]) && !is_array($loc[$tag]) && !isset($dataKey))
					{
						$loc[$tag] = array($loc[$tag]);
					}
					if (isset($dataKey))
					{
						if (!$addAttributes)
						{
							$loc[$tag][$dataKey] = $this->_xmlParseData['val']['value'];
						}
						else
						{
							$loc[$tag][$dataKey] = array('attributes' => $this->_xmlParseData['val']['attributes'], 
																  'xml_value' => $this->_xmlParseData['val']['value']);
						}
					}
					else
					{
						if (!$addAttributes)
						{
							$loc[$tag][] = $this->_xmlParseData['val']['value'];
						}
						else
						{
							$loc[$tag][] = array('attributes' => $this->_xmlParseData['val']['attributes'], 
																  'xml_value' => $this->_xmlParseData['val']['value']);
						}
					}
				}
				// Add new element
				else
				{
					if (!$addAttributes)
					{
						$loc[$tag] = $this->_xmlParseData['val']['value'];
					}
					else
					{
						$loc[$tag] = array('attributes' => $this->_xmlParseData['val']['attributes'], 
															  'xml_value' => $this->_xmlParseData['val']['value']);
					}
				}
				
			}
			// Tag without data
			else if ($this->_xmlParseData['val']['type'] == 'complete')
			{
				$this->_processOpen();
				$this->_processClose();
			}
		}
    
    // move root sub-elements up a level
    if ($this->_includeRootTag) {
      $keys = array_keys($this->_data);
      if (isset($this->_data[$keys[0]][0])) {
        $this->_data = array($keys[0] => $this->_data[$keys[0]][0]);
      }
    }
    
    // move single elements up 1 level
    $keys = array_keys($this->_data);
    foreach($keys as $key) {
      // not needed
      //$this->_checkForSingleLevel($this->_data[$key]);
    }
		
		// Write to cache
		$this->_cache();
		
    }
    // }}}

    // {{{ _processClose()
    /**
     * Method used to process an xml close tag.
     *
     * @access  private
     * @return  void
     * @author  Jason Read <jason@idir.org>
     */
    function _processClose()
    {
		// don't include top tag
		if ($this->_xmlParseData['topTag'] && !$this->_includeRootTag && $this->_xmlParseData['val']['tag'] == $this->_xmlParseData['topTag'])
		{
			return false;
		}
		if (isset($this->_xmlParseData['currTag']) && array_key_exists($this->_xmlParseData['currTag'], $this->_xmlParseData['multipleData']) && 
		    $this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple'])
		{
			$tkeys = array_reverse(array_keys($this->_xmlParseData['multipleData']));
			foreach ($tkeys as $tkey)
			{
				if ($this->_xmlParseData['multipleData'][$tkey]['multiple'] && !$this->_xmlParseData['multipleData'][$tkey]['popped'])
				{
					array_pop($this->_xmlParseData['levels']);
					$this->_xmlParseData['multipleData'][$tkey]['popped'] = true;
					break;
				}
				else if (!$this->_xmlParseData['multipleData'][$tkey]['multiple'])
				{
					break;
				}
			}
		}
		
		// Pop any additional key level attributes
		$this->_xmlParseData['prevTag'] = array_pop($this->_xmlParseData['levels']);
		if (strpos($this->_xmlParseData['prevTag'], 'arr#'))
		{
			$this->_xmlParseData['prevTag'] = array_pop($this->_xmlParseData['levels']);
		}
		
		return true;
    }
    // }}}

    // {{{ _processOpen()
    /**
     * Method used to process an xml open tag.
     *
     * @access  private
     * @return  void
     * @author  Jason Read <jason@idir.org>
     */
    function _processOpen()
    {
		// Set root tag
		if (!$this->_xmlParseData['topTag'])
		{
			$this->_rootTag = strtolower($this->_xmlParseData['val']['tag']);
		}
		
		// don't include top tag
		if (!$this->_xmlParseData['topTag'] && !$this->_includeRootTag)
		{
			$this->_xmlParseData['topTag'] = $this->_xmlParseData['val']['tag'];
			return false;
		}
		$this->_xmlParseData['currTag'] = $this->_xmlParseData['val']['tag'];
		$this->_xmlParseData['currTag'] = strtolower($this->_xmlParseData['val']['tag']);
		$this->_xmlParseData['levels'][] = $this->_xmlParseData['currTag'];
		
		// Add attributes array
		$addAttributes = false;
		unset($dataKey);
		if (array_key_exists('attributes', $this->_xmlParseData['val']))
		{
			if (array_key_exists('KEY', $this->_xmlParseData['val']['attributes']))
			{
				$dataKey = $this->_getKey($this->_xmlParseData['val']['attributes']['KEY']);
			}
			if (count($this->_xmlParseData['val']['attributes']) >= 1)
			{
				$keys = array_keys($this->_xmlParseData['val']['attributes']);
				foreach ($keys as $key)
				{
					// Check for embedded php code
					if (strstr($this->_xmlParseData['val']['attributes'][$key], 'php::'))
					{
						$this->_xmlParseData['val']['attributes'][$key] = preg_replace_callback("'php::(.*?)::php'si", 'codeToString', 
																					 $this->_xmlParseData['val']['attributes'][$key]);
					}
					$this->_xmlParseData['val']['attributes'][strtolower($key)] = $this->_xmlParseData['val']['attributes'][$key];
					unset($this->_xmlParseData['val']['attributes'][$key]);
				}
				$addAttributes = true;
			}
		}
		
		// Multiple items w/ same name. Convert to array
		$dataKeyPushed = false;
		if ($this->_xmlParseData['prevTag'] === $this->_xmlParseData['currTag'])
		{
			if ((!array_key_exists($this->_xmlParseData['currTag'], $this->_xmlParseData['multipleData']) || 
				!$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple']) && !isset($dataKey))
			{
				$loc =& $this->_data;
				foreach ($this->_xmlParseData['levels'] as $level)
				{
					$temp =& $loc[$level];
					$loc =& $temp;
				}
				$loc = array($loc);
				$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple'] = true;
				$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple_count'] = 0;
			}
			$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['popped'] = false;
			if (!isset($dataKey))
			{
				$this->_xmlParseData['levels'][] = ':arr#' . ++$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple_count'];
			}
			else
			{
				$dataKeyPushed = true;
				$this->_xmlParseData['levels'][] = ':arr#' . $dataKey;
			}
		}
		else
		{
			$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple'] = false;
		}
		
		if (isset($dataKey))
		{
			if (!isset($loc))
			{
				$loc = false;
			}
			$loc = array($dataKey => $loc);
			if (!$dataKeyPushed)
			{
				$this->_xmlParseData['levels'][] = ':arr#' . $dataKey;
			}
			$this->_xmlParseData['prevTag'] = $this->_xmlParseData['currTag'];
			$this->_xmlParseData['currTag'] = $dataKey;
			$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple'] = true;
			$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['multiple_count'] = 1;
			$this->_xmlParseData['multipleData'][$this->_xmlParseData['currTag']]['popped'] = false;
		}
		
		if ($addAttributes)
		{
			$loc =& $this->_data;
			foreach ($this->_xmlParseData['levels'] as $level)
			{
				$temp =& $loc[str_replace(':arr#', '', $level)];
				$loc =& $temp;
			}
			$loc['attributes'] = $this->_xmlParseData['val']['attributes'];
		}
		
		return true;
    }
    // }}}

}
// }}}

/* Keep this comment at the end of the file.
Local variables:
mode:c++
minor-mode:font-lock
indent-tabs-mode:nil
c-basic-offset:4
End:
*/
?>
